﻿
/*==============================================================================
Copyright (c) Microsoft Corporation. All rights reserved.

This source code is intended only for use with Microsoft
Development Tools and/or on-line documentation.  See these other
materials for detailed information regarding Microsoft code samples.

THIS CODE AND INFORMATION ARE PROVIDED "AS IS."  YOU BEAR THE RISK OF USING IT.  
MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTIES OR CONDITIONS.  
TO THE EXTENT PERMITTED UNDER YOUR LOCAL LAWS, MICROSOFT EXCLUDES 
THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NON-INFRINGEMENT.
=============================================================================================*/
/**********************************************************************************************
Module:  BackToBackProvider.cs
Description: The BackToBackProvider class corresponds to the SDP plane of the BackToBackUA. It is
responsible for returning a final SDP Answer for any initial or subsequent SDP Offer pushed to the 
provider.
The backtobackprovider essentially acts as a broker. It is capable of handling multiple early media
Answers (aka provisional Answers) coming from remote endpoints reached through OCS forking
(MPOP, team ring, etc) as a result of a given Offer. It is also capable of handling renegotiations 
during call establishment and once the call is established. SDP renegotiation allows UC endpoints
to add and remove modalities (e.g. Video or Audio) within a dialog, but also to put a call on hold
or resume the call.

The back to back provider is also capable of relaying Video Frame Freeze and Key Frame requests when
the B2B call is connected to Audio Video MCU in order to support switching from one Video source to 
another.
**********************************************************************************************/


using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Mime;
using Microsoft.Rtc.Collaboration;
using Microsoft.Rtc.Collaboration.ComponentModel;
using Microsoft.Rtc.Signaling;

namespace BackToBack
{
    /// <summary>
    /// BackToBackProvider is the SDP broker used by BackToBack User Agent.
    /// </summary>
    /// <remarks> 
    /// One BackToBackProvider instance is meant to be bound to both an incoming call and outgoing call.
    /// </remarks>
    internal class BackToBackProvider : MediaProvider
    {
        #region Private Fields
        private object _syncRoot = new object();

        /// Audio and Video are the only media types supported by this back-to-back MediaProvider
        private static string[] s_supportedMediaTypes = { MediaType.Audio, MediaType.Video, "applicationsharing"};

        private SdpOffer _initialIncomingOffer;

        /// This dictionary keeps track of information relevant to AsyncOperations spanning across two calls. This information 
        /// is keyed on a DialogContext. 
        private Dictionary<DialogContext, BackToBackAsyncOperationInfo> _listOfBackToBackAsyncOperations;

        #endregion

        #region Constructors
        /// <summary>
        /// BackToBackProvider constructor.
        /// </summary>
        internal BackToBackProvider()
        {
            this.ListOfBackToBackAsyncOperations = new Dictionary<DialogContext, BackToBackAsyncOperationInfo>();
        }
        #endregion

        #region Properties
        /// <summary>
        /// Dictionary of B2BProvider operation information.
        /// </summary>
        private Dictionary<DialogContext, BackToBackAsyncOperationInfo> ListOfBackToBackAsyncOperations
        {
            get { return _listOfBackToBackAsyncOperations; }
            set { _listOfBackToBackAsyncOperations = value; }
        }

        /// <summary>
        /// The SDP Offer received by the incoming Call and pushed to the back to back provider upon binding the provider 
        /// to the incoming Call.
        /// </summary>
        private SdpOffer InitialIncomingOffer
        {
            get { return _initialIncomingOffer; }
            set { _initialIncomingOffer = value; }
        }
        #endregion

        #region MediaProvider Interface Implementation

        /// <summary>
        /// List of media types supported by the BackToBackProvider.
        /// </summary>
        public override IEnumerable<string> SupportedMediaTypes
        {
            get { return s_supportedMediaTypes; }
        }

        /// <summary>
        /// SetIncomingCallOffer is being called when the incoming call of the back to back is received. The Offer is pushed to the B2BProvider
        /// when the BackToBackProvider is bound to the incoming call. BackToBackProvider simply caches the initial Offer until it is passed to 
        /// the outgoing Call.
        /// </summary>
        protected override void SetIncomingCallOffer(OfferAnswerContext offerAnswerContext, SdpOffer offer)
        {
            this.InitialIncomingOffer = offer;
        }


        /// <summary>
        /// BeginGetAnswer is being called as a result of the B2BUA invoking BeginEstablishEarlyMedia or BeginAccept on the incoming call in reaction 
        /// to provisional responses or a successful final response received on the outbound call of the B2BUA.
        /// It is also being invoked when an SDP renegotiation is received from a remote endpoint.
        /// </summary>
        /// <returns></returns>
        protected override IAsyncResult BeginGetAnswer(OfferAnswerContext offerAnswerContext, SdpOffer offer, AsyncCallback userCallback, object state)
        {
            //Getting a provisional or a final SDP Answer as a result of the initial SDP Offer pushed to the backTobackprovider.
            if (offerAnswerContext.Reason == SdpOfferAnswerReason.IncomingCallEstablishment)
            {
                //In case of renegotiation, we need to maintain the association between DialogContext and CallDialogContext.
                BackToBackAsyncOperationInfo callDialogContextAssociation = null;
                if (!FindBackToBackAsyncOperation(offerAnswerContext.CallDialogContext.DialogContext, out callDialogContextAssociation))
                {
                    callDialogContextAssociation = new BackToBackAsyncOperationInfo();
                    AddBackToBackAsyncOperation(offerAnswerContext.CallDialogContext.DialogContext, callDialogContextAssociation);
                    callDialogContextAssociation.CallDialogContext = offerAnswerContext.CallDialogContext;
                }
  
                // Retrieve the DialogContext in the outgoing call that maps to the current DialogContext of the incoming Call.
                // The mapping of dialogs through cross conferencing is performed in the SIP plane by the BackToBackUA.
                DialogContext correspondingDialogContext = offerAnswerContext.CallDialogContext.DialogContext.ApplicationContext as DialogContext;

                // Then, retrieve the outstanding B2BProvider async operation information associated with the corresponding DialogContext.
                BackToBackAsyncOperationInfo answerOperation = null;
                if (!FindBackToBackAsyncOperation(correspondingDialogContext, out answerOperation))
                {
                    //this should not happen
                    throw new OfferAnswerException(OfferAnswerFailureReason.ServerInternalError, "Could not find corresponding outstanding operation.", null);
                }

                SdpAnswer newAnswer = null;

                if (offerAnswerContext.IsEarlyDialog == true)
                {
                    // It is an early dialog, a provisional Answer is requested.
                    newAnswer = new SdpAnswer(answerOperation.ProvisionalAnswer.Answer, answerOperation.ProvisionalAnswer.Status);
                    answerOperation.ProvisionalAnswer = null;

                }
                else
                {
                    // It is confirmed dialog, a final Answer is requested.
                    newAnswer = new SdpAnswer(answerOperation.FinalAnswer.Answer, answerOperation.FinalAnswer.Status);

                    // Once server receives a final-response, clean-up the unnecessary
                    // dialogs created as a result of forking. This only needs to be
                    // done the first time final-answer is received. 
                    List<DialogContext> dialogContextsTokeep = new List<DialogContext>(2);
                    dialogContextsTokeep.Add(offerAnswerContext.CallDialogContext.DialogContext);
                    dialogContextsTokeep.Add(correspondingDialogContext);
                    CleanupUnnecessaryDialogContexts(dialogContextsTokeep);
                }
     
                // Synchronously and successfully complete the AsyncResult operation 
                AsyncResult<SdpAnswer> asyncResult = new AsyncResult<SdpAnswer>(userCallback, state);
                asyncResult.SetAsCompleted(newAnswer, true);
                return asyncResult;
            }
            else if (offerAnswerContext.Reason == SdpOfferAnswerReason.IncomingRenegotiation)
            {
                //A renegotiation request was received.

                // Store the relevant information context to perform the renegotiation operation across Call1 and Call2.
                BackToBackAsyncOperationInfo renegotiationOperation = null;
                if (!FindBackToBackAsyncOperation(offerAnswerContext.CallDialogContext.DialogContext, out renegotiationOperation))
                {
                    renegotiationOperation = new BackToBackAsyncOperationInfo();
                    AddBackToBackAsyncOperation(offerAnswerContext.CallDialogContext.DialogContext, renegotiationOperation);
                }

                //Create an async result to handle this renegotiation operation.
                AsyncResult<SdpAnswer> asyncResult = new AsyncResult<SdpAnswer>(userCallback, state);

                //Caching the offer and Async Result.
                renegotiationOperation.Offer = offer;
                renegotiationOperation.AsyncResult = asyncResult;

                //Retrieve the corresponding dialog context on the other Call.
                DialogContext correspondingDialogContext = offerAnswerContext.CallDialogContext.DialogContext.ApplicationContext as DialogContext;
                
                //Then, retrieve the association between the DialogContext and CallDialogContext for the corresponding dialog context.
                BackToBackAsyncOperationInfo callDialogContextAssociation = null;
                if (!FindBackToBackAsyncOperation(correspondingDialogContext, out callDialogContextAssociation))
                {
                    //log
                    //this should never happen.
                    throw new OfferAnswerException(OfferAnswerFailureReason.ServerInternalError, "Could not find corresponding outstanding operation.", null);
                }


                try
                {
                    // Initiate the renegotiation operation on the other Call in order to obtain an Sdp Answer from the other remote endpoint
                    // in response to the new Offer received.
                    this.BeginSdpRenegotiation(callDialogContextAssociation.CallDialogContext,
                                               SdpRenegotiationCompleted,
                                               renegotiationOperation);
                }
                catch (InvalidOperationException ioex)
                {
                    //log
                    // Probably the corresponding call is already terminated. B2Bprovider completes
                    // the pending AsyncResult in such case with 'NotAcceptable'. Eventually
                    // this call will get terminated as well.
                    asyncResult.SetAsCompleted(new OfferAnswerException(OfferAnswerFailureReason.NotAcceptable,
                                                "Call to MP.BeginSdpRenegotiation() failed - corresponding call probably got terminated",
                                                ioex),
                                                true);
                    
                    //clearing the cache for efficiency.
                    renegotiationOperation.Offer = null;
                    renegotiationOperation.AsyncResult = null;
                }

                return asyncResult;
            }
            else
            {   //log
                //this should never happen
                throw new OfferAnswerException(OfferAnswerFailureReason.ServerInternalError, "Unexpected BeginGetAnswer invokation", null);
            }

        }

        /// <summary>
        /// Return a provisional or a final Answer. It may throw, if no Answer was received from the B2BUA destination.
        /// </summary>
        /// <returns>returns an Sdp Answer if successful.</returns>
        protected override SdpAnswer EndGetAnswer(IAsyncResult result)
        {
            AsyncResult<SdpAnswer> asyncResult = result as AsyncResult<SdpAnswer>;

            return asyncResult.EndInvoke();
       }

        /// <summary>
        /// BeginGetOffer is called initially as a result of the outbound call being initiated to forward the initial SDP Offer received on the
        /// incoming call. The method is then being called every time a renegotiation is started on a call in reaction to renegotiation request 
        /// received on the other call.
        /// </summary>
        /// <returns></returns>
        protected override IAsyncResult BeginGetOffer(OfferAnswerContext offerAnswerContext, Collection<SdpContentDescription> lastLocalOutgoingSdps, AsyncCallback userCallback, object state)
        {
            if (offerAnswerContext.Reason == SdpOfferAnswerReason.OutgoingCallEstablishment)
            {
                // The Oubound call of the B2BUA is being initiated, the initial Offer is being forwarded to the remote endpoint.
                SdpOffer newOffer = null;

                // This is the first attempt to forward the initial Offer received.
                if (lastLocalOutgoingSdps == null)
                {
                    // First attempt, we get the incoming call offer received and send it to the B2BUA destination.
                    newOffer = new SdpOffer(this.InitialIncomingOffer.Offers, null);
                    this.InitialIncomingOffer = null;
                }
                else
                {
                    // This is the second attempt to forward a trimmed down version of the initial Offer because 
                    // the destination could not process multiple offers (probably by lack of support of M/MIME bodies).
                    // OCS 2007 R2 endpoints order the SDP Offers as follows: ICEv6 Sdp Offer followed by the ICEv19 one 
                    // in multi-part MIME format. Receiving '415 - Unsupported Media Type' means that receiving party 
                    // doesn't understand the multi-part MIME most likely because the destination endpoint is a OCS 2007 
                    // (or below) endpoint.
                    // Therefore, backToback provider truncates the multi-part message and present only the first ICEv6 offer  
                    // that OCS 2007 and below endpoints understand.
                    newOffer = new SdpOffer(lastLocalOutgoingSdps[0] , null); /*pick the ICEv6 offer*/
                }
                
                // Create a new AsyncResult for the operation.
                AsyncResult<SdpOffer> asyncResult = new AsyncResult<SdpOffer>(userCallback, state);
                asyncResult.SetAsCompleted(newOffer, true);
                return asyncResult;

            }
            else if (offerAnswerContext.Reason == SdpOfferAnswerReason.OutgoingRenegotiation)
            {
                // BeginGetOffer() here is being invoked following a renegotiation request.
                DialogContext correspondingDialogContext = offerAnswerContext.CallDialogContext.DialogContext.ApplicationContext as DialogContext;

                BackToBackAsyncOperationInfo renegotiationB2BDialogInfo = null;
                if (FindBackToBackAsyncOperation(correspondingDialogContext, out renegotiationB2BDialogInfo))
                {
                    SdpOffer newOffer = new SdpOffer(renegotiationB2BDialogInfo.Offer.Offers, null);

                    AsyncResult<SdpOffer> asyncResult = new AsyncResult<SdpOffer>(userCallback, state);
                    asyncResult.SetAsCompleted(newOffer, true);
                    return asyncResult;
                }
                else
                {
                  //log this should not happen.
                  throw new OfferAnswerException(OfferAnswerFailureReason.ServerInternalError,"Could not find the corresponding renegotiation context",null);
                }
            }
            else
            {
               // log
               // this should not happen
               throw new OfferAnswerException(OfferAnswerFailureReason.ServerInternalError,"Unexpected BeginGetOffer() operation",null);
            
            }

        }

        /// <summary>
        /// EndGetOffer returns an Sdp Offer to the Call.
        /// </summary>
        protected override SdpOffer EndGetOffer(IAsyncResult result)
        {
            AsyncResult<SdpOffer> asyncResult = result as AsyncResult<SdpOffer>;
            return asyncResult.EndInvoke();
        }


        /// <summary>
        /// SetAnswer is being invoked to notify the backToback provider that an SDP Answer (provisional or final) has been received
        /// from the the remote endpoint in response to an Offer forwarded by the backToBackProvider. It is also invoked when an Sdp Answer
        /// is being received in response to a renegotiation request.
        /// </summary>
        protected override void SetAnswer(OfferAnswerContext offerAnswerContext, SdpOffer originalOffer, SdpAnswer answer)
        {
            if (offerAnswerContext.Reason == SdpOfferAnswerReason.OutgoingCallEstablishment)
            {
                // An Answer is received in response to the initial Offer forwarded with the outbound call.

                // Retrieve or Create an object to store the Answer in the context of the DialogContext where the Answer was received. 
                BackToBackAsyncOperationInfo answerOperation = null;
                if (!FindBackToBackAsyncOperation(offerAnswerContext.CallDialogContext.DialogContext, out answerOperation))
                {
                    answerOperation = new BackToBackAsyncOperationInfo();
                    this.AddBackToBackAsyncOperation(offerAnswerContext.CallDialogContext.DialogContext, answerOperation);
                }

                if (answer.Status == SdpAnswerStatus.Provisional)
                {
                    answerOperation.ProvisionalAnswer = answer;
                }
                else if (answer.Status == SdpAnswerStatus.Final)
                {
                    answerOperation.FinalAnswer = answer;
                }

                //Storing the DialogContext / CallDialogContext association.
                answerOperation.CallDialogContext = offerAnswerContext.CallDialogContext;

            }
            else if (offerAnswerContext.Reason == SdpOfferAnswerReason.OutgoingRenegotiation)
            {
                // An SDP Answer is being received in the context of a renegotiation.

                // Retrieve the dialog context of the other call involved in the renegotiation
                DialogContext correspondingDialogContext = offerAnswerContext.CallDialogContext.DialogContext.ApplicationContext as DialogContext;

                // Retrieve the renegotiation operation associated with the Sdp Answer received
                BackToBackAsyncOperationInfo renegotiationOperation = null;
                if (FindBackToBackAsyncOperation(correspondingDialogContext, out renegotiationOperation))
                {
                    AsyncResult<SdpAnswer> asyncResult = renegotiationOperation.AsyncResult;

                    if (asyncResult != null)
                    {
                        if (answer.Status == SdpAnswerStatus.Final || answer.Status == SdpAnswerStatus.Provisional)
                        {
                            // An SDP Answer has been successfully received.
                            asyncResult.SetAsCompleted(answer, false); //this should complete the outstanding renegotiation operation.

                            //clear the cache for efficiency.
                            renegotiationOperation.AsyncResult = null;
                            renegotiationOperation.Offer = null;

                        }
                        else
                        {
                            // A failure response has been received, there is no SDP Answer to carry over.
                            // Translate rest of the response-codes into an exception and store 
                            // the exception in asyncResult

                            OfferAnswerException oaex = CreateOfferAnswerExceptionBasedOnFailureResponse(answer.Status);
                            asyncResult.SetAsCompleted(oaex, false);

                            //clear the cache for efficiency.
                            renegotiationOperation.AsyncResult = null;
                            renegotiationOperation.Offer = null;

                        }
                    }
                }
                else
                {
                    throw new OfferAnswerException(OfferAnswerFailureReason.ServerInternalError, "SetAnswer operation is not expected", null);
                }
            }
        }


        /// <summary>
        /// BeginTerminateMedia is implemented but not really used as the termination of the BackToBackUA takes care of terminating the BackToBack
        /// </summary>
        /// <returns></returns>
        /// //
        // Summary:
        //     Initiates an asynchronous media termination operation.
        //
        // Parameters:
        //   callDialogContext:
        //     The signaling context for the session on which media should be terminated.
        //
        //   isTerminatingSignalingSession:
        //     A Boolean flag that indicates whether the underlying SignalingSession will
        //     be terminated after terminating the media session.
        //
        //   userCallback:
        //     The method to be called when the asynchronous operation is completed.
        //
        //   state:
        //     A user-provided object that distinguishes this particular asynchronous operation
        //     from other asynchronous operations.
        //
        // Returns:
        //     An IAsyncResult that references the asynchronous operation.
        //
        protected override IAsyncResult BeginTerminateMedia(CallDialogContext callDialogContext, bool isTerminatingSignalingSession, AsyncCallback userCallback, object state)
        {
            //no-op async operation
            AsyncResultNoResult asyncResult = new AsyncResultNoResult(userCallback, state);
            asyncResult.SetAsCompleted(null, true);
            return asyncResult;
        }


        protected override void EndTerminateMedia(IAsyncResult result)
        {
            AsyncResultNoResult asyncResult = result as AsyncResultNoResult;
            asyncResult.EndInvoke();
        }

        /// <summary>
        /// HandleMessage is implemented in order to forward Video Key Frame and Frame Freeze requests that are sent through INFO requests.
        /// </summary>
        protected override bool HandleMessage(CallDialogContext session, MessageReceivedEventArgs eventArgs)
        {
          if ((eventArgs.MessageType == MessageType.Info) && (0 == String.Compare(eventArgs.RequestData.ContentType.MediaType, "application/media_control+xml", StringComparison.OrdinalIgnoreCase)))
          {
            //Setting returning automatic response to false so that the response from the remote endpoint is used instead.
            eventArgs.DisableAutomaticResponse = true;           
            DialogContext correspondingDialogContext = session.DialogContext.ApplicationContext as DialogContext;

            BackToBackAsyncOperationInfo callContextAssociation = null;
            if (!FindBackToBackAsyncOperation(correspondingDialogContext, out callContextAssociation))
            {
              //log
              return false;
            }

            try
            {
                this.BeginSendMessage(callContextAssociation.CallDialogContext, MessageType.Info, new ContentDescription(eventArgs.RequestData.ContentType, eventArgs.RequestData.GetMessageBody()), null, MessageSentCompleted, eventArgs);
                return true;
            }
            catch(RealTimeException e)
            {
               //log 
                return true;
            }
          }
          else
          {
              return false;
          }
        }
        #endregion

        #region Private Methods
        private void SdpRenegotiationCompleted(IAsyncResult asyncResult)
        {
            BackToBackAsyncOperationInfo renegotiationOperation = asyncResult.AsyncState as BackToBackAsyncOperationInfo;

            try
            {
                this.EndSdpRenegotiation(asyncResult);
            }
            catch (RealTimeException rtex)
            {

                // For all failure response codes that don't map to enum 'SdpAnswerStatus',
                // OOTY doesn't call MP.SetAnswer() to notify the failure reason but instead
                // throws in MP.EndSdpRenegotiation() throws in such cases with the failure reason.
                // B2BProvider completes the pending AsyncResult for all these failure reasons with
                // one reason: 'NotAcceptable'.

                renegotiationOperation.AsyncResult.SetAsCompleted(new OfferAnswerException(OfferAnswerFailureReason.NotAcceptable, "Call to MP.EndSdpRenegotiation() failed with exception", rtex), false);
            }
        }


        /// <summary>
        /// MessageSentCompleted is the callback invoked when send message completes.
        /// </summary>
        /// <param name="asyncresult"></param>
        private void MessageSentCompleted(IAsyncResult asyncresult)
        {
            MessageReceivedEventArgs eventargs = asyncresult.AsyncState as MessageReceivedEventArgs;

            SipResponseData data= null;

            try
            {
                data = this.EndSendMessage(asyncresult);
            }
            catch (RealTimeException e)
            {
                //log
            }
            finally
            {
                try
                {
                    if (data != null)
                    {
                        eventargs.SendResponse(data.ResponseCode, data.ContentType, data.GetMessageBodyString(), null);
                    }
                    else
                    {
                        eventargs.SendResponse(ResponseCode.Success);
                    }
                }
                catch (InvalidOperationException ioex)
                {
                  //log
                }

                catch (RealTimeException rtex)
                { 
                  //log
                }
            }
          
        }

        /// <summary>
        /// This method should only be called for failure responses.
        /// </summary>
        private OfferAnswerException CreateOfferAnswerExceptionBasedOnFailureResponse(SdpAnswerStatus answerStatus)
        {
            switch (answerStatus)
            {
                case SdpAnswerStatus.NotAcceptable:
                    {
                        return new OfferAnswerException(
                            OfferAnswerFailureReason.NotAcceptable,
                            "Failed SDP Negotiation request: 488 - Not Acceptable", null);
                    }
                case SdpAnswerStatus.NotAcceptableLocally:
                    {
                        return new OfferAnswerException(
                            OfferAnswerFailureReason.NotAcceptable,
                            "Failed SDP Negotiation request: 488 - Not Acceptable", null);
                    }
                case SdpAnswerStatus.RequestPending:
                    {

                        return new OfferAnswerException(
                            OfferAnswerFailureReason.RequestPending,
                            "Failed SDP Negotiation request: 491 - Detected Offer/Answer collision", null);
                    }
                case SdpAnswerStatus.UnsupportedMediaCanRetry:
                case SdpAnswerStatus.UnsupportedMediaCannotRetry:
                    {

                        return new OfferAnswerException(
                            OfferAnswerFailureReason.UnsupportedMedia,
                            "Failed SDP Negotiation request: 415 - Unsupported Media Type", null);
                    }
                default:
                    {
                        // Map unknown failure reasons to 'ServerInternalError'
                        return new OfferAnswerException(
                            OfferAnswerFailureReason.ServerInternalError,
                            String.Format("Failed SDP Negotiation request: Unknown failure reason ",
                                          answerStatus.ToString()), null);
                    }
            }
        }

        private void AddBackToBackAsyncOperation(DialogContext key, BackToBackAsyncOperationInfo value)
        {
            lock (_syncRoot)
            {
                this.ListOfBackToBackAsyncOperations.Add(key, value);
            }
        }

        private bool RemoveBackToBackAsyncOperation(DialogContext key)
        {
            bool success = false;

            lock (_syncRoot)
            {
               success = this.ListOfBackToBackAsyncOperations.Remove(key);
            }

            return success;
        }

        private bool FindBackToBackAsyncOperation(DialogContext key, out BackToBackAsyncOperationInfo found)
        {
            bool success = false;
            lock (_syncRoot)
            {
                success = this.ListOfBackToBackAsyncOperations.TryGetValue(key, out found);
                    
            }
            return success;
        }

        /// <summary>
        /// Delete the preserved mappings from the dictionary for rest of the 
        /// dialog-contexts, except the ones asked to keep. This method should be
        /// called to clean-up extra resources (contexts) that are not needed 
        /// anymore, after receiving the final successful response from one of 
        /// the dialog contexts.
        /// </summary>
        private void CleanupUnnecessaryDialogContexts(List<DialogContext> dialogContextsToKeep)
        {
            lock (_syncRoot)
            {
                DialogContext[] arrayOfDialogContexts = new DialogContext[this.ListOfBackToBackAsyncOperations.Count];
                this.ListOfBackToBackAsyncOperations.Keys.CopyTo(arrayOfDialogContexts, 0);

                foreach (DialogContext item in arrayOfDialogContexts)
                {
                    if (!dialogContextsToKeep.Contains(item))
                    {
                        this.ListOfBackToBackAsyncOperations.Remove(item);
                    }
                }
            }
        }
        #endregion

        #region private class BackToBackDialogInfo (Inner class)

        /// <summary>
        /// BackToBackAsyncOperationInfo represents information relevant to a particular DialogContext, that is cached to perform
        /// async operations spanning across multiple calls.
        /// </summary>
        private class BackToBackAsyncOperationInfo
        {
            private SdpOffer _offer;
            private SdpAnswer _finalAnswer;
            private SdpAnswer _provisionalAnswer;
            private AsyncResult<SdpAnswer> _asyncResult;   //outstanding async operation 
            private CallDialogContext _callDialogContext;

            internal BackToBackAsyncOperationInfo()
            {
            }

            internal CallDialogContext CallDialogContext
            {
              get {return _callDialogContext;}
              set {_callDialogContext = value;}
        
            }

            internal SdpOffer Offer
            {
                get { return _offer; }
                set { _offer = value; }
            }

            internal SdpAnswer FinalAnswer
            {
                get { return _finalAnswer; }
                set { _finalAnswer = value; }
            }

            internal SdpAnswer ProvisionalAnswer
            {
                get { return _provisionalAnswer; }
                set { _provisionalAnswer = value; }
            }

            internal AsyncResult<SdpAnswer> AsyncResult
            {
                get { return _asyncResult; }
                set { _asyncResult = value; }
            }
        }
        #endregion
    }
}